Hey there!
This lecture is going to discuss a slightly more advanced topic.
We are going to explore coroutines, and a coroutine is created by the coroutine library, and a coroutine
is represented by a data type in Lua called a thread.
Now the thread data type doesn't actually represent what we would think of as a processor thread.
The creators of Lua just gave it the name of thread, but it isn't actually a processor thread.
I know it's confusing.
Instead, a coroutine is a function or a block of code that can have its flow manipulated.
Coroutines allow us to execute multiple tasks within the same script.
In fact, all code executed in your game is handled by different coroutine threads, which are having
their flow manipulated behind the scenes in the game engine.
When we have code in a script, all of the code inside of the script is being executed from one coroutine
thread.
Every script in your game has its own coroutine.
The problem with this is that if we have a while loop that executes infinitely, any code below the
while loop will never execute.
And that's because the main thread of execution is currently busy and it's stuck working inside of the
while loop.
Even if we hit a yield statement inside of the while loop, the current processor thread will just move
to the next coroutine that needs to be executed.
And since all of the code inside of our script is being executed on the same coroutine, the code below
our while loop will never execute.
It's not in a separate coroutine thread.
If you hear me, use the terms current thread or current processor thread or main thread of execution.
I am referring to the actual processor thread that is being fed instructions by the Lua interpreter.
If you are confused by any of this terminology, I have a cheat sheet document at the end of this section
that you can use to understand these terms more clearly.
Now back to our while loop.
We are able to use the coroutine library to create a new coroutine thread.
That way we can split this while loop into a separate coroutine and have more than one coroutine executing
inside of our script, and this gives us the ability to control the flow of these coroutine threads.
So I'm going to place this while loop in a separate coroutine thread.
Don't worry what I'm doing right now, because you're going to learn how to do this in just a bit.
But I'm going to use a function in the coroutine library called create.
And I need to pass a function to it.
And I'm going to place my while loop inside of there.
And this is going to return to me my thread data type.
Then I'm going to go ahead and start this coroutine by using a function called resume.
And I'm going to pass my thread.
And now my while loop is executing in a separate coroutine thread.
So the code in here is in its own coroutine thread.
And the code down here is also in its own coroutine thread.
Since we now have more than one coroutine when the yield statement is reached inside of our first coroutine
thread, the main thread of execution is able to go down and execute the code below the while loop.
Because this code is existing in a different coroutine.
Now, I don't expect you to use coroutines that often in studio, as they can be difficult to debug.
And there are other alternatives that are more user friendly, like the task library, which we're going
to see in a future lecture.
Inside of the Roblox documentation, we are able to read what exactly a coroutine is, and it tells
us that a coroutine is used to perform multiple tasks at the same time from within the same script.
So what I just discussed such tasks might include producing values from inputs or performing work on
a subroutine.
When solving a larger problem, a task doesn't even need to have a defined ending point, but it does
need to define particular times at which it yields or pause to let other things be worked on.
So, as I discussed earlier in our while loop, the code below the while loop won't execute until the
coroutine that is currently using the main thread of execution to execute code yields or pauses.
And as we saw in our example, that yield statement was the task dot wait function, and it tells us
that a new coroutine can be created by providing a function to the coroutine dot create function.
Once created, a coroutine doesn't begin running until the first call to the coroutine dot resume function,
which passes the arguments to the function.
This call returns when the function either halts or calls coroutine dot yield, and when this happens,
coroutine dot resume returns either the values returned by the function, the values sent to coroutine
dot yield, or an error message.
If it does error, the second return value is the thrown error.
Now a coroutine has four different states suspended, running normal, and dead.
When you first create a coroutine using the coroutine dot create function, the thread is going to be
in a suspended state, and then when you start the coroutine or resume it, then it's going to be placed
in the running state.
If the coroutine yields, then it'll go back to being suspended.
And when a coroutine finally finishes executing code, or it returns, or maybe it encounters an error,
then the coroutine is going to be placed in a dead state and it can't be used anymore.
The normal state is going to be when a coroutine gives up control over the main thread of execution,
and resumes a different coroutine thread, and it's waiting for that one to yield.
So let's go ahead and take a look at the different functions within the coroutine library.
For example, this first one is called coroutine dot close and you have to pass to it a coroutine thread
and it says closes and puts the provided coroutine in a dead state, so it closes out or stops the execution
within a coroutine.
The next one here is called create and it says creates a new coroutine with body f.
So we have to pass a function to the create function in order to create a new coroutine thread.
There's a function called is yield able and it says returns true.
If the coroutine this function is called within can safely yield.
So you can call this function whenever inside of any coroutine thread.
And it's going to return a boolean.
If the coroutine can yield, there is another function called coroutine dot resume and it says starts
or continues the execution of coroutine Co.
So we pass a coroutine thread to this and then we can pass any other arguments to that particular coroutine.
We have a function called coroutine dot running and it says returns the running coroutine.
So whenever your code is executing, if you want it to grab the current coroutine thread, you can just
call coroutine dot running and it'll return that value to you.
We have a function called coroutine status and it says returns the status of a coroutine as a string.
So we pass our coroutine.
And then it's going to tell us whether or not the coroutine is suspended, if it's running, if it's
in the normal state, or if it's in the dead state.
The next function we have is called wrap and it says creates a new coroutine and returns a function
that, when called, resumes the coroutine.
So we pass a function to the wrap function and it returns to us another function.
And we are able to call this function to start the execution of the coroutine.
So this does not return a thread data type, but another function.
And because we can just call that function to resume the coroutine, this means we don't have to use
the coroutine dot resume function.
And the last but not least is the coroutine dot yield function, and it says suspends the execution
of the coroutine.
And we're able to pass other information to this function.
So let's go ahead and take a look at all of these different functions in action in Roblox Studio.
So with our first example here I used the coroutine dot create function and passed a function to it
that contained our while loop, and it returned to us a coroutine thread, which is going to execute
this block of code in a separate coroutine.
And then to start this coroutine thread, we have to pass it to the coroutine dot resume function.
So now this while loop will start executing.
Now what I'm going to do is I'm going to put a print statement in here.
I'm going to call this start execution.
Or we could just say, actually, you know, let's put a message in here while loop doing stuff.
The while loop is doing whatever it needs to do.
And then once we hit this yield statement, it'll go back down to this code and execute it and print
it in the console.
So this statement should print first and then this statement should print first.
If I moved this yield statement above the print statement then this will actually print first.
And then this will print second.
Because if you think about it the main thread of execution is going to go in here, create this new
coroutine thread and then we're told to resume it.
So the current thread of execution is going to hop in here.
It'll go into the while loop.
And then it'll immediately hit our yield statement and be like oh well, it's time for me to leave.
Let's see if there's anything else that needs to be executed.
And since our code down here is waiting to execute because it's in a separate coroutine thread, then
the main thread is going to print hi in the console.
And once this yield statement up here is over, it'll come back and print this in the console.
If we flip it around.
Now what's going to happen is the main thread of execution is going to encounter this statement.
It's going to print it into the console.
Then it'll yield and print this in the console.
So let's go ahead and run this game.
And if we take a look at the messages in the console we get while loop doing stuff, then hi.
And then you can see our while loop continuing to do more stuff multiple times every single second.
Now if I move the position of the yield statement above, then we should see hi print first and then
while loop doing stuff will print second.
So let's go ahead and run.
And there we go, high printed first.
Then we get our print statements of while loop doing stuff.
Let's go ahead and take a look at another example of using coroutines.
Let's go ahead and create a new coroutine thread.
So we'll refer to the coroutine library and use the dot create function.
And we need to pass a function to it.
So we'll create an anonymous function here.
And inside of this function I'm going to do is have a for loop that iterates five times.
And what we'll do inside of this for loop is we're going to call the coroutine dot yield function.
And what we're going to do is we're going to pass AI to this function call.
And then once this for loop is done executing at the end, we're going to return a message like finished.
So now this is going to return to us a thread data type.
And we need to use the coroutine dot resume function to start executing the code inside of this thread.
So we can go ahead and call coroutine dot resume and pass the thread here.
Now what's going to happen is that the main thread of execution is going to go through here.
And it's going to start this thread and it's going to go in.
And the first statement is going to execute is this coroutine dot yield function call.
And we're passing I to it.
What is that going to do.
Well it's going to force this coroutine to yield.
And we're going to return back to our coroutine dot resume function call with this value.
So that means what is going to be returned from the coroutine dot resume function is going to be our
value of I.
But there is also going to be another value that is returned from the coroutine resume function.
And that's a boolean.
So let's first make a variable for the boolean.
And then we're also able to grab our number I.
And then we can print this out inside of the console.
So we'll print bool and I.
If we go ahead and run the game and we take a look in the console, we're going to get true.
And then our first index in that for loop which is the number one.
Now our coroutine is not going to continue executing this for loop until we are explicitly told to resume
this coroutine, again using our coroutine dot resume function.
Now you might be wondering what this boolean represents, and the boolean represents whether or not
our coroutine encountered an error.
And that's because coroutines are automatically protected when they encounter an error.
If a coroutine does encounter an error, then this boolean is going to be set to false.
And the second argument is going to be the error message or whatever happened that caused the coroutine
to halt execution, which is what we read in the documentation.
Now, to be able to execute this coroutine fully, I need to call my coroutine dot resume function five
times, because I'm yielding through every iteration of our for loop.
So let's go ahead and make a repeat loop down here.
And we're going to repeatedly call our coroutine dot resume function on this particular coroutine thread.
And then we're also able to print out the results from the coroutine resume function into the console.
So every single time this should print out true.
And then one.
And then next time it'll print true and the number two.
And then the next time true and the number three and so on until our coroutine finishes executing in
the for loop.
And then we'll return finished.
And the coroutine will be placed in a dead state.
So let's go ahead and continue resuming our coroutine until our coroutine is placed in the dead state.
And we can check that by using the coroutine dot status function.
So we can pass our thread here, and it's going to return to us a string of what state it is in.
So we can pass our thread here.
And we can check to see if that is equal to the dead state.
So we're only going to resume the coroutine as long as the status of the coroutine is not equal to the
dead state.
Now I also want to print out the status of our coroutine before we resume it.
So we're going to do coroutine dot status and pass our thread.
And every single time through the iteration of this repeat loop, this should print out suspended because
we have not resumed our coroutine yet.
And then we go and resume it.
We print the result in the console, and then through the next iteration, it'll realize, oh, the
coroutine is suspended again.
Let's go ahead and resume it and get the next value and the next value until eventually we finish executing
the coroutine.
And then let's go ahead and print out a statement like coroutine is in a dead state.
Now if we run our game.
What are we going to get?
Ooh, look at this.
We get first print statement, which is suspended.
Then we get our values of true and one.
Then we get suspended again.
True.
And two, all the way up to five iterations.
And then our coroutine reaches the end and it returns to us the message of finished.
And our repeat loop realizes that our coroutine has been placed in a dead state.
So it states coroutine is in a dead state.
Now, let's say our coroutine encounters an error while it's executing this code up here.
So I'm going to forcefully insert an error in here.
I'm going to call the error function.
And we'll just say a message like oh no, an error occurred.
And then let's go ahead and run the game again.
What you're going to see this time is that it's going to successfully loop through that entire for loop.
But then right before we are able to reach the return statement inside of our function, it's going
to print out false because the coroutine encountered an error.
And it's going to tell us exactly what that coroutine error was.
Oh no.
An error occurred on line number five.
And as you can see, it did not halt the execution of our game because the coroutine was protected and
it still printed that the coroutine was placed in a dead state because the coroutine encountered an
error.
Let's go ahead and take a look at another example.
For this example, I want to grab the current coroutine that is executing the code in my script.
And remember we can do that by using the coroutine running function which returns the current running
coroutine.
So let's store that in a variable.
We'll call it current coroutine.
And then what I want to do is I want to force this coroutine to yield and then resume it later.
And in order to do that we're going to need to have two separate coroutines.
The separate one is going to resume it after some arbitrary amount of time.
So what we can go ahead and do is call coroutine dot yield, which is going to force the coroutine currently
executing the script to yield.
But before we yield it, let's go ahead and have another function above.
Wait for some random amount of time and then resume it again, which means we'll be able to execute
any code after the coroutine dot yield call.
So we could print something like coroutine was resumed.
And for this example, let's go ahead and take a look at the coroutine dot wrap function.
And for the wrap function we need to pass a function to it.
So let's create a function.
And inside of here what we're going to do is we're going to wait for a random amount of time let's say
five seconds.
And then we're going to use the coroutine dot resume function to.
Resume our current coroutine.
And then this is going to return to us a another function we'll call this wrapped function.
And then to start this coroutine up here we need to call this function.
So we'll refer to wrap function.
And then we'll just call it.
Now what's going to happen is the current thread is going to go in here.
It's going to start executing this separate coroutine thread right here.
It's going to encounter this yield statement.
So it's going to leave this coroutine.
And then it'll come back.
And then it'll hit our yield statement here which is going to force the rest of our script to pause.
And then after this five seconds is up, then it's going to resume that current coroutine that we just
paused.
And then we'll see.
The message of coroutine was resumed printed back in the console.
Now I also want to demonstrate that we can go ahead and pass values to the resume function.
As you can see, there are three dots here telling us that we can pass whatever values we want.
So let's pass a message and say something like I have resumed you from.
Your friendly coroutine.
So this is our message that we want to give back to the other coroutine.
And what's going to happen is that this message is going to be given back to this function here.
And we can store that inside of a variable.
We'll just call it message.
And then let's print that message out in the console.
So if you remember what we read in the documentation, is that any time a coroutine encounters a coroutine
dot yield function call, it's going to pause right here.
And then any time another coroutine resumes it, it is able to pass other information back to this coroutine
dot yield function call.
So this when we call this is going to return back whatever was passed to the coroutine dot resume function.
In that case it's going to be our message.
So let's go ahead and run the game.
And so far nothing is being printed, which is what we would expect.
And then there we go.
It says coroutine was resumed.
The message is I have resumed you from your friendly coroutine.
And actually, let's demonstrate this again.
Before we yield, let's go ahead and print a message that the current coroutine is about to be paused.
Then we'll pause it and then five seconds later we will resume it.
So let's try this again.
We'll get our message.
Coroutine is about to be paused.
And now we're waiting.
And there we go.
Five seconds later it says coroutine was resumed and then we get our message.
Very cool.
Now, something important I want to note is that the function returned by our coroutine dot wrap function
is not protected from errors like a regular coroutine thread is, and that's because the wrap function
is just returning a regular function to us.
So to demonstrate this, let's go ahead and delete what we currently have.
And let's create another wrapped function.
So we'll call coroutine dot wrap.
Pass a function here inside of this function I'm just going to force an error.
And we could just say some error.
And then I'm also going to put a return statement down here and return some value like we'll say Bobby.
And then we can go ahead and store our wrapped function in a variable.
And let's go ahead and see if we can print out the return value from our wrapped function.
So when we call it, what's going to happen is we're going to hit this error.
And since coroutine dot wrapped does not have any protection against errors, the entire coroutine that
is executing inside of our script is going to halt, and it's going to print out an error message inside
of the console.
And that means we're not going to be able to reach our return statement of Bobby.
So if we run the game and we take a look, there we go.
We get our error on line number six and it says some error.
And that's back to our code or our function on line number two.
So as you can see it did not return a true or false boolean.
And it did not return to us an error message as a string.
And that's because they are not protected against errors.
Now if I go ahead and delete this error message, then when I call my wrapped function, I should get
Bobby printed inside of the console.
So if we run the game, let's take a look.
There we go.
We get Bobby.
Now you should also notice we did not get a boolean because once again wrapped functions are not protected
against errors.
Hopefully you're starting to get the hang of coroutines.
So let's go ahead and look at another example.
What I'm going to do is I'm going to create a function.
I'm going to call this function repeat string.
And we're going to pass a string to it.
We'll just call it a word.
Now what I want this function to do is I want it to continually repeat whatever word was passed here
over and over and over again.
So for example.
If we get past the word of Bob, then we want to concatenate it with another word of Bob and then another
one.
So on for infinity.
And we want to do this with coroutines of mine.
So we can go ahead and do is have a while loop down here.
And what we're going to do is we're going to continually concatenate this word together to get a bigger
and bigger and bigger repeating word.
Now I also want to go ahead and pass this function to our coroutine dot create function.
So we'll pass repeat string to this, which is going to give us a brand new coroutine thread.
And because this is now executing in its own coroutine thread, we are able to call the coroutine dot
yield function and get a result.
In this case, we want to get our word that has been concatenated together.
So let's go ahead and create another variable.
We'll call this repeated.
And for now I'm going to set it to an empty string because inside of our while loop through every single
iteration, we're going to update the value inside of here to be concatenated to itself.
So we'll set repeated equal to repeated concatenated with our word.
And it's going to keep doing this through every iteration of our loop, which is going to give us a
bigger and bigger word that has been repeating.
Now another way we can do this that is quicker.
We can use the dot dot equal operator, which does the exact same thing of what we just saw right here.
It's just more concise syntax.
But now what we need to go ahead and do is we need to start the execution of our coroutine thread.
So let's go ahead and call the coroutine dot resume function.
And we're going to pass our thread here.
And then we also need to pass other information to our thread.
In this case that is going to be our word.
So what word would we like to repeat.
Well let's go ahead and repeat the word of Bob.
And then we're able to print what gets returned from here.
Because what we're going to do is when we call our coroutine dot yield function, let's pass this repeated
word.
So that means it's going to return our word back to the coroutine dot resume function call.
And we should be able to print it inside of the console.
So if we run our game, what we're going to see inside of the console is true.
And then there's our word Bob.
So through the first iteration of our while loop, of course there's only going to be one bob concatenated
to our empty string.
But let's say we copy this and do it again.
But this time we don't actually need to put Bob right here, because it's already going to have the
word inside of our thread.
We're just resuming it again because it hit that yield statement.
So it's going to go through the next iteration of the loop and concatenate it again and then return
that back to us.
So this should print Bob.
Bob, if we run the game we should get Bob.
And there we go Bob.
Bob.
Well, what if we resume the thread again and again and again?
Let's see what happens this time.
Now we're going to get Bob, Bob, Bob, Bob Bob Bob Bob Bob Bob.
You see, it's concatenating Bob every single time we are resuming that coroutine thread.
So actually let's do something a little bit different here.
Let's create a for loop that iterates, let's say 50 times.
And all we're going to do is just resume.
This coroutine.
So coroutine dot resume.
Pass our thread here.
So now we should have Bob Bob Bob printing out 50 different times inside of our console getting bigger
every single time.
So let's run it and see what happens.
Oh there we go.
We get our 50 print statements.
First iteration is Bob, then Bob, Bob, so on and so forth as it continues to concatenate Bob to itself.
And we just have this giant triangle of bob strings.
All right.
So that was a lot of information to digest.
And hopefully you now understand the coroutine library and the thread data type.
If you would like to learn more about coroutines I'll leave a resource attached to this lecture that
goes to the official Lua documentation, which details coroutines in depth.
In the future, however, we will be taking a look at something called the Task Library, which you
will likely be using much more often than coroutines in your scripts.
Otherwise, thanks for watching and I'll see you in the next lecture.